package edu.cmu.arch.pipesandfilters;

import java.io.IOException;
import java.util.ArrayList;

import edu.cmu.arch.pipesandfilters.frame.Frame;
import edu.cmu.arch.pipesandfilters.frame.Variable;

/**
 *  The ExtrapolateFilter filters wild points out of the data stream.
 *  It replaces the wild points with extrapolated data.
 *  this filter has two output ports
 *  	OutputPort: outputs the extrapolated data
 *      OutputPort2: outputs the wildpoints detected
 *  
 *   @author Steven Thomas (steventh@andrew.cmu.edu)
 */
public class ExtrapolateFilter extends DualOutputFilterFramework {

	ArrayList<Frame> Buffer = new ArrayList<Frame>(); // holds data while searching for the next valid point
	Variable _MeasurementToEvaluate;  // the measurement we are evaluating: i.e. pressure
	Double _LowerBound;       // the lower limit for detecting wild points
	Double _UpperBound;       // the upper limit for detecting wild points
	Double _LastValidValue;   // holds the last valid data point, used to adjust wildpoint data


	/**
	 * The default constructor.
	 * @param MeasurementToEvaluate the specific measurement used for detecting wildpoints, ie. Pressure
	 * @param LowerBound the lower bound for setting a range of valid data points
	 * @param UpperBound the upper bound for setting a range of valid data points
	 */
	public ExtrapolateFilter(Variable MeasurementToEvaluate, Double LowerBound, Double UpperBound)
	{
		//TODO: Ensure LowerBound is less than UpperBound

		_MeasurementToEvaluate = MeasurementToEvaluate;
		_LowerBound = LowerBound;
		_UpperBound = UpperBound;
	}
	
	/*
	 * (non-Javadoc)
	 * @see edu.cmu.arch.pipesandfilters.DualOutputFilterFramework#runFilter1()
	 */
	@Override
	protected void runFilter1() {
		Frame frame = null;
		while (true)
		{
			try
			{
				frame = readFrame();
				processFrame(frame);
		    }
			catch (EndOfStreamException e)
			{
			    //file processing is complete	
				runFilter1Finished=true;
				System.out.print( "\n" + this.getName() + "::Sink Exiting -- \n" );
				break;
	
			} // catch
			catch (IOException ioe)
			{
				System.out.println(this.getName() + "::runFilter1() IOException occured; Unable to read frame" + ioe.getMessage());
				break;
			}
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see edu.cmu.arch.pipesandfilters.DualOutputFilterFramework#runFilter2()
	 */
	@Override
	protected void runFilter2() {
		    // Filter2 waits around for filter1 to be done before it closes
		    while(!runFilter1Finished)
		    {
		       try {
					sleep(1);
				} catch (InterruptedException e) {
					System.out.println(this.getName() + "::runFilter2() InterruptedException occured" + e.getMessage());
				}
			}
		    
			ClosePorts();
		    System.out.println("Filter Out Port2 exiting");
		    this.runFilter2Finished = true;
	}

   /**
    * Sends a message to Filter2 to write out reject data point
    * @param frame
    * @throws IOException 
    */
   private void writeOutOfRangeFrames(Frame frame) 
   {
	   FrameFilterFramework filter2 = (FrameFilterFramework)this.getOutputFilter2();
	   try
	   {
		   filter2.writeFrame(frame);
	   }
	   catch(IOException e) {
		   System.out.println(this.getName() + "::writeOutOfRangeFrames() IOException occured; Error writing to OutPort 2" + e.getMessage());
	}
   }

    /**
     * Processes a single frame.
     * 	1) If Value not in Range 
	 *      a) If not, then find the next valid value
	 *      b) Send the frame to Port2
	 *      c) Extrapolate a value
	 *      d) update the frame and mark as extrapolated
     *  2) If Value in range, store it for future use
     *  3) write the frame to Port1
     * @param frame
     */
	private void processFrame(Frame frame) 
	{
		Double MeasurementValue = 
			(Double)frame.getValue(_MeasurementToEvaluate);
		
		if (MeasurementValue == null) {
			throw new IllegalArgumentException("Frame is missing required value: " 
					+ _MeasurementToEvaluate.getColumnName());
		}
		
		if(!isInRange(MeasurementValue))
		{
			//message filter 2 to write a frame
			writeOutOfRangeFrames(frame);
			
			Double NextValidValue = getNextValidValue();
			Double ExtrapolatedValue;
			
			if(_LastValidValue==null)
			{
				ExtrapolatedValue = NextValidValue;
			} 
			else if(NextValidValue == null)
			{
				ExtrapolatedValue = _LastValidValue;
			}
			else
			{
				ExtrapolatedValue  =(Double)(NextValidValue + _LastValidValue)/2;
			}
			// update frame and mark as extrapolated 
			writeExtrapolatedFrame(frame,ExtrapolatedValue);
			
			//process buffer
			// searching for a nextvalidvalue fills the buffer, so process any values
			processBuffer(ExtrapolatedValue);
		}
		else
		{
			_LastValidValue = MeasurementValue;
		    try {
				writeFrame(frame);
			} catch (IOException e) {
				System.out.println(this.getName() + "::processFrame() IOException occured" + e.getMessage());
			}
		}
		
	}
	
	/**
	 * Returns True if data is in the valid range
	 * Returns False if data is outside the valid range
	 * 
	 * @param ValueToCheck a value to check
	 * @return true if is value in range
	 */
	private Boolean isInRange(Double ValueToCheck)
	{
		return ((ValueToCheck > this._LowerBound)&&(ValueToCheck < this._UpperBound));
	}
	
	/**
	 * Searches the stream for a valid value. As it reads the stream it puts data 
	 * to the buffer for later processing.   
	 * @return Double the next valid value within range, null if no next valid value found
	 */
	private Double getNextValidValue()
	{
	
		// keep reading from the stream until we find a valid measurement
		// or the end of the stream has been reached
		Frame frame;
		while(true)
		{
			try{
				frame = readFrame();
				Buffer.add(frame);
				
			    if(isInRange((Double)frame.getValue(_MeasurementToEvaluate)))
			    {
			    	//valid measurement found
			    	return (Double)frame.getValue(_MeasurementToEvaluate);
			    }
			    
			    	//if
			}catch(EndOfStreamException e)
			{
				// we will reach here if we didn't find a valid value and the stream ended
				// returns null if no valid value found
				return null;
			}catch (IOException e) {
				System.out.println(this.getName() + "::getNextValidValue() IOException occured" + e.getMessage());
			}
		}//while
	}
	
	/**
	 * Process all data in the buffer.  All data in the buffer represents frames with
	 * the evaluated measurement outside the valid range.  Write wildpoints to port2 
	 * and update the evaluated measurement with the ExtrapolatedValue
	 * 
	 * @param ExtrapolatedValue The value to use when modifying invalid values
	 */
	private void processBuffer(Double ExtrapolatedValue)
	{
		
		while(!Buffer.isEmpty())
		{
			//pop a frame off the buffer
			Frame frame = Buffer.remove(0);
			Double MeasurementValue = 
				(Double)frame.getValue(_MeasurementToEvaluate);
			
			if(isInRange(MeasurementValue))
			{
				
				try {
					writeFrame(frame);
				} catch (IOException e) {
					System.out.println(this.getName() + "::processBuffer() IOException occured" + e.getMessage());
				}
				
			}
			else
			{
				writeOutOfRangeFrames(frame);
				//update frame value and mark as extrapolated
				writeExtrapolatedFrame(frame,ExtrapolatedValue);
			}
		}
	}
	
	/**
	 * Updates a frame with the Extrapolated value, marks the frame as extrapolated
	 * Sends to Port1
	 * @param frame 
	 * @param ExtrapolatedValue
	 */
	private void writeExtrapolatedFrame(Frame frame,Double ExtrapolatedValue)
	{
		frame.putValue(_MeasurementToEvaluate, ExtrapolatedValue);
		frame.putValue(Variable.ExtrapolatedId, _MeasurementToEvaluate.getId());
		try {
			writeFrame(frame);
		}catch (IOException e)
		{
			System.out.println(this.getName() + "::writeExtrapolatedFrame() IOException occured; Error writing to buffer" + e.getMessage());
		}
	}
	
	

}

